// Copyright 2013 Vedran Vuk. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// Package IRC implements an IRC client.
package irc

import (
	"bufio"
	"crypto/tls"
	"errors"
	"fmt"
	"net"
	s2 "code.google.com/p/vvrepo/strings"
)

// IRC command string constants.
const (
	PING    = "PING"
	NICK    = "NICK"
	USER    = "USER"
	PONG    = "PONG"
	JOIN    = "JOIN"
	PART    = "PART"
	KICK    = "KICK"
	PRIVMSG = "PRIVMSG"
	NOTICE  = "NOTICE"
	QUIT    = "QUIT"
	WEBIRC  = "WEBIRC"
)

// Max IRC message length to send. Messages are either limited or split into
// multiple messages and sent, depending on command.
const MaxMsgLen = 400

// OnRaw is a prototype for an event that fires every time a command is sent
// or received. Complete line is in "txt". If command is outgoing "out" is true.
type OnRaw func(msg IRCMessage, out bool)

// OnJoin is a prototype for an event that fires when you or another user
// defined by "ep" joins a channel "ch".
type OnJoin func(ch string, ep IRCEntity)

// OnPart is a prototype for an event that fires when you or another user
// defined by "ep" parts a channel "ch" with an optional part message "msg".
type OnPart func(ch, msg string, ep IRCEntity)

// OnPart is a prototype for an event that fires when you or another user
// get kicked from a channel.
type OnKick func(user IRCEntity, ch, reason string)

// OnMsg is a prototype for an event that fires when you or another user
// defined by "src" sends a message "msg" to target "tgt".
type OnMsg func(msg string, src, tgt IRCEntity)

// OnNick is a prototype for an event that fires when you or another user
// changes a nickname.
type OnNick func(user IRCEntity, newNick string)

// OnQuit is a prototype for an event that fires when you or another user
// defined by "src" quits the IRC with an optional Quit message "msg".
type OnQuit func(msg string, src IRCEntity)

// IRC defines an IRC client.
type IRC struct {
	Nick string
	User string
	Name string
	Mode string

	OnRaw    OnRaw
	OnJoin   OnJoin
	OnPart   OnPart
	OnKick   OnKick
	OnMsg    OnMsg
	OnNotice OnMsg
	OnNick   OnNick
	OnQuit   OnQuit

	IAL    IAL

	conn   net.Conn
	reader *bufio.Reader
}

// Creates a new IRC instance. All parameters except "name" must be ansi and
// with limited special characters. See RFC1459, (2810, 2812, 2813) for details.
//
// Nickname "nick" is the only mandatory field. It uniquely identifies the user
// on an IRC network. Maximum length depends from network to network, usually
// limited to 11 characters.
//
// Username "user" is optional and copied from "nick" if empty. It was usually
// set by IRC client software from the local running identd server, back in the
// days. Max length is 11 ansi chars depending from network from network.
//
// RealName "name" is optional and copied from "nick" if empty. It further
// identifies a user on an IRC network. There is no charset restriction.
//
// UserMode "mode" is in the form of {+|-{mode}} where + enables
// and - disables a list of one or more characters defining a user mode.
// Example: +xi-w or +i or -B
// Usually clients set this to +i only (invisible). Supported user modes vary
// from ircd to ircd and unsupported ones are simply ignored by the server.
func NewIRC(nick, user, name, mode string, doDefHandlers bool) (*IRC, error) {
	if nick == "" {
		return nil, errors.New("Nick not specified.")
	}
	r := &IRC{Nick: nick, User: user, Name: name, Mode: mode}
	if user == "" {
		r.User = r.Nick
	}
	if name == "" {
		r.Name = r.Nick
	}
	if mode == "" {
		r.Mode = "+"
	}
	if doDefHandlers {
		r.OnRaw = func(msg IRCMessage, out bool) {
			if out {
				fmt.Printf("-> %s\r\n", msg.String())
			} else {
				fmt.Printf("<- %s\r\n", msg.String())
			}
		}
		r.OnJoin = func(ch string, ep IRCEntity) {
			fmt.Printf("%s Joins %s\n", ch, ep)
		}
		r.OnPart = func(ch, msg string, ep IRCEntity) {
			fmt.Printf("%s Parts %s (%s)\n", ch, msg, ep)
		}
		r.OnKick = func(user IRCEntity, ch, reason string) {
			fmt.Printf("%s kicked from %s: %s", user.Nickname(), ch, reason)	
		}
		r.OnMsg = func(msg string, src, tgt IRCEntity) {
			fmt.Printf("%s Msg %s: %s\n", src, tgt, msg)
		}
		r.OnNotice = func(msg string, src, tgt IRCEntity) {
			fmt.Printf("%s Notice %s: %s\n", src, tgt, msg)
		}
		r.OnNick = func(user IRCEntity, newNick string) {
			fmt.Printf("%s Changed nick to %s: %s\n", user.Nickname(), newNick)
		}
		r.OnQuit = func(msg string, src IRCEntity) {
			fmt.Printf("%s Quits (%s)\n", src, msg)
		}
	}
	return r, nil
}

// Dial connects to an IRC server. The method has the same signature as
// net.Conn.Dial() with an extra parameter for a TLS configuration.
// If "tlscfg" is nil an unsecure connection is made, otherwise a TLS secured
// connection is made with properties specified in "tlscfg".
func (irc *IRC) Dial(network, addr string, tlscfg *tls.Config) error {
	var err error
	if tlscfg == nil {
		irc.conn, err = net.Dial(network, addr)
	} else {
		irc.conn, err = tls.Dial(network, addr, tlscfg)
	}
	if err != nil {
		return err
	}
	irc.reader = bufio.NewReader(irc.conn)

	// Register immediately after connecting.
	irc.SendRaw(fmt.Sprintf("NICK %s", irc.Nick))
	irc.SendRaw(fmt.Sprintf("USER %s %s * :%s", irc.User, irc.Mode, irc.Name))

	return nil
}

// Continuously reads from the remote endpoint while connected and dispatches
// local event handlers. This function must be called as a goroutine immediately
// after the Dial() call and will return only after the connection is closed
// either by remote or after a local IRC.Close() call.
func (irc *IRC) Run() error {
	for {
		s, err := irc.reader.ReadString('\n')
		if err != nil {
			irc.reader = nil
			return err
		}
		m := NewIRCMessage(s)
		if irc.OnRaw != nil {
			irc.OnRaw(m, false)
		}
		switch {
		case m.Command() == PING:
			if m.HasTrailing() {
				irc.SendRaw(fmt.Sprintf("%s :%s", PONG, m.Trailing()))
			} else {
				irc.SendRaw(PONG)
			}
		case m.Command() == JOIN:
			irc.IAL.AddUserToChannel(m.Prefix(), m.Trailing(), 0)
			if irc.OnJoin != nil {
				irc.OnJoin(m.Trailing(), *m.Prefix())
			}
		case m.Command() == PART:
			if s2.CompareFold(m.Prefix().Nickname(), irc.Nick) == 0 {
				irc.IAL.DeleteChannel(m.Middle())	
			} else {
				irc.IAL.DeleteUserFromChannel(m.Prefix().Nickname(), m.Middle())
			}
			if irc.OnPart != nil {
				irc.OnPart(m.Middle(), m.Trailing(), *m.Prefix())
			}
		case m.Command() == KICK:			
			if s2.CompareFold(m.Prefix().Nickname(), irc.Nick) == 0 {
				irc.IAL.DeleteChannel(m.Middle())	
			} else {
				irc.IAL.DeleteUserFromChannel(m.Prefix().Nickname(), m.Middle())
			}
			if irc.OnKick != nil {
				irc.OnKick(*m.Prefix(), m.Middles()[0], m.Trailing())
			}
		case m.Command() == PRIVMSG:
			if irc.OnMsg != nil {
				irc.OnMsg(m.Trailing(), *m.Prefix(), IRCEntity(m.Middles()[0]))
			}
		case m.Command() == NOTICE:
			if irc.OnNotice != nil {
				irc.OnNotice(m.Trailing(), *m.Prefix(), IRCEntity(m.Middles()[0]))
			}
		case m.Command() == NICK:
			irc.IAL.ChangeUserNickname(m.Prefix().Nickname(), m.Trailing())
			if irc.OnNick != nil {
				irc.OnNick(*m.Prefix(), m.Trailing())
			}
		case m.Command() == QUIT:
			irc.IAL.DeleteUser(m.Prefix().Nickname())
			if irc.OnQuit != nil {
				irc.OnQuit(m.Trailing(), *m.Prefix())
			}
		case m.Numeric() == 353:
			// :pie.anonops.li 353 puppytest = #ludnica :~evilworks &Hal9000 puppytest
			ch := ""
			// Some IRCDs don't send "=" between target and channel params.
			if len(m.Middles()) == 3 {
				ch = m.Middles()[2]
			} else {
				ch = m.Middles()[1]
			}
			for _, v := range m.Trailings() {
				a, b := ParseChannelModes(v)
				e := IRCEntity(a + "!@")
				irc.IAL.AddUserToChannel(&e, ch, b)
			}			
		}		
	}
	return nil
}

// Close closes the TCP connection. It is recommended to call Quit() instead to
// have the IRC server close the connection instead. In both cases prior Run()
// call will return.
func (irc *IRC) Close() error {
	return irc.conn.Close()
}

// SendRaw sends a command "cmd" and terminates it with a carriage return
// and line feed combination as required by the IRC protocol.
func (irc *IRC) SendRaw(cmd string) {
	if irc.conn == nil {
		return
	}
	s := fmt.Sprintf("%s\r\n", LimitMsg(cmd, 510))
	if irc.OnRaw != nil {
		irc.OnRaw(IRCMessage(fmt.Sprintf("LEN: %d; %s", len(s), s)), true)
	}
	irc.conn.Write([]byte(s))
}

// Join a channel "ch" with an optional channel key "key".
func (irc *IRC) Join(ch, key string) {
	if key != "" {
		irc.SendRaw(fmt.Sprintf("%s %s %s", JOIN, ch, key))
	} else {
		irc.SendRaw(fmt.Sprintf("%s %s", JOIN, ch))
	}
}

// Part the channel "ch" with an optional part message "msg".
func (irc *IRC) Part(ch, msg string) {
	if msg != "" {
		irc.SendRaw(fmt.Sprintf("%s %s :%s", PART, ch, msg))
	} else {
		irc.SendRaw(fmt.Sprintf("%s %s", PART, msg))
	}
}

// Send message "msg" to target "tgt" where target can be a channel name or
// a nickname.
func (irc *IRC) PrivMsg(tgt, msg string) {
	max := MaxMsgLen - len(fmt.Sprintf("%s %s :", PRIVMSG, tgt))
	out := SplitMsg(msg, max)
	for _, v := range out {
		irc.SendRaw(fmt.Sprintf("%s %s :%s", PRIVMSG, tgt, v))
	}
}

// Send notice "msg" to target "tgt" where target can be a channel name or
// a nickname.
func (irc *IRC) Notice(tgt, msg string) {
	max := MaxMsgLen - len(fmt.Sprintf("%s %s :", NOTICE, tgt))
	out := SplitMsg(msg, max)
	for _, v := range out {
		irc.SendRaw(fmt.Sprintf("%s %s :%s", NOTICE, tgt, v))
	}
}

// Change your nickname.
func (irc *IRC) Nickname(newNick string) {
	irc.SendRaw(fmt.Sprintf("%s :%s", NICK, newNick))
}

// Quit IRC with an optional quit message "msg".
func (irc *IRC) Quit(msg string) {
	if msg != "" {
		irc.SendRaw(fmt.Sprintf("%s :%s", QUIT, msg))
	} else {
		irc.SendRaw(QUIT)
	}
}
